import { HttpHandler, HttpRequest, HttpResponse } from "@smithy/protocol-http";
import { buildQueryString } from "@smithy/querystring-builder";
import type { AbortSignal, HttpAgent, HttpsAgent, Logger, NodeHttpHandlerOptions } from "@smithy/types";
import { HttpHandlerOptions, Provider } from "@smithy/types";
import { getTransformedHeaders } from "./get-transformed-headers";
import { timing } from "./timing";
import { http } from "@kit.NetworkKit";
import { buffer, stream, util } from "@kit.ArkTS";
import { streamCollector } from "./stream-collector";

export { NodeHttpHandlerOptions };

interface ResolvedNodeHttpHandlerConfig extends Omit<NodeHttpHandlerOptions, "httpAgent" | "httpsAgent"> {
  // httpAgent: HttpAgent;
  // httpsAgent: HttpsAgent;
}

/**
 * @public
 * A default of 0 means no timeout.
 */
export const DEFAULT_REQUEST_TIMEOUT = 0;

/**
 * @public
 * A request handler that uses the Node.js http and https modules.
 */
export class NodeHttpHandler implements HttpHandler<NodeHttpHandlerOptions> {
  private config?: ResolvedNodeHttpHandlerConfig;
  private configProvider: Promise<ResolvedNodeHttpHandlerConfig>;
  private socketWarningTimestamp = 0;

  // Node http handler is hard-coded to http/1.1: https://github.com/nodejs/node/blob/ff5664b83b89c55e4ab5d5f60068fb457f1f5872/lib/_http_server.js#L286
  public readonly metadata = { handlerProtocol: "http/1.1" };

  /**
   * @returns the input if it is an HttpHandler of any class,
   * or instantiates a new instance of this handler.
   */
  public static create(
    instanceOrOptions?: HttpHandler<any> | NodeHttpHandlerOptions | Provider<NodeHttpHandlerOptions | void>
  ) {
    if (typeof (instanceOrOptions as any)?.handle === "function") {
      // is already an instance of HttpHandler.
      return instanceOrOptions as HttpHandler<any>;
    }
    // input is ctor options or undefined.
    return new NodeHttpHandler(instanceOrOptions as NodeHttpHandlerOptions);
  }

  /**
   * @internal
   *
   * @param agent - http(s) agent in use by the NodeHttpHandler instance.
   * @param socketWarningTimestamp - last socket usage check timestamp.
   * @param logger - channel for the warning.
   * @returns timestamp of last emitted warning.
   */
//   public static checkSocketUsage(
//     agent: HttpAgent | HttpsAgent,
//     socketWarningTimestamp: number,
//     logger: Logger = console
//   ): number {
//     // note, maxSockets is per origin.
//     const { sockets, requests, maxSockets } = agent;
//
//     if (typeof maxSockets !== "number" || maxSockets === Infinity) {
//       return socketWarningTimestamp;
//     }
//
//     const interval = 15_000;
//     if (Date.now() - interval < socketWarningTimestamp) {
//       return socketWarningTimestamp;
//     }
//
//     if (sockets && requests) {
//       for (const origin in sockets) {
//         const socketsInUse = sockets[origin]?.length ?? 0;
//         const requestsEnqueued = requests[origin]?.length ?? 0;
//
//         /**
//          * Running at maximum socket usage can be intentional and normal.
//          * That is why this warning emits at a delay which can be seen
//          * at the call site's setTimeout wrapper. The warning will be cancelled
//          * if the request finishes in a reasonable amount of time regardless
//          * of socket saturation.
//          *
//          * Additionally, when the warning is emitted, there is an interval
//          * lockout.
//          */
//         if (socketsInUse >= maxSockets && requestsEnqueued >= 2 * maxSockets) {
//           logger?.warn?.(
//             `@smithy/node-http-handler:WARN - socket usage at capacity=${socketsInUse} and ${requestsEnqueued} additional requests are enqueued.
// See https://docs.aws.amazon.com/sdk-for-javascript/v3/developer-guide/node-configuring-maxsockets.html
// or increase socketAcquisitionWarningTimeout=(millis) in the NodeHttpHandler config.`
//           );
//           return Date.now();
//         }
//       }
//     }
//
//     return socketWarningTimestamp;
//   }

  constructor(options?: NodeHttpHandlerOptions | Provider<NodeHttpHandlerOptions | void>) {
    this.configProvider = new Promise((resolve, reject) => {
      if (typeof options === "function") {
        options()
          .then((_options) => {
            resolve(this.resolveDefaultConfig(_options));
          })
          .catch(reject);
      } else {
        resolve(this.resolveDefaultConfig(options));
      }
    });
  }

  private resolveDefaultConfig(options?: NodeHttpHandlerOptions | void): ResolvedNodeHttpHandlerConfig {
    const { requestTimeout, connectionTimeout, socketTimeout, socketAcquisitionWarningTimeout, httpAgent, httpsAgent } =
      options || {};
    const keepAlive = true;
    const maxSockets = 50;

    return {
      connectionTimeout,
      requestTimeout: requestTimeout ?? socketTimeout,
      socketAcquisitionWarningTimeout,
      // httpAgent: (() => {
      //   if (httpAgent instanceof hAgent || typeof (httpAgent as hAgent)?.destroy === "function") {
      //     return httpAgent as hAgent;
      //   }
      //   return new hAgent({ keepAlive, maxSockets, ...httpAgent });
      // })(),
      // httpsAgent: (() => {
      //   if (httpsAgent instanceof hsAgent || typeof (httpsAgent as hsAgent)?.destroy === "function") {
      //     return httpsAgent as hsAgent;
      //   }
      //   return new hsAgent({ keepAlive, maxSockets, ...httpsAgent });
      // })(),
      logger: console,
    };
  }

  destroy(): void {
    // this.config?.httpAgent?.destroy();
    // this.config?.httpsAgent?.destroy();
  }
  async handle(request: HttpRequest, { abortSignal }: HttpHandlerOptions = {}): Promise<{ response: HttpResponse }> {
    if (!this.config) {
      this.config = await this.configProvider;
    }

    return new Promise(async (_resolve, _reject) => {
      let writeRequestBodyPromise: Promise<void> | undefined = undefined;

      // Timeouts related to this request to clear upon completion.
      const timeouts :number[]= [] ;

      const resolve = async (arg: { response: HttpResponse }) => {
        await writeRequestBodyPromise;
        timeouts.forEach(timing.clearTimeout);
        _resolve(arg);
      };
      const reject = async (arg: unknown) => {
        await writeRequestBodyPromise;
        timeouts.forEach(timing.clearTimeout);
        _reject(arg);
      };

      if (!this.config) {
        throw new Error("Node HTTP request handler config is not resolved");
      }

      // if the request was already aborted, prevent doing extra work
      if (abortSignal?.aborted) {
        const abortError = new Error("Request aborted");
        abortError.name = "AbortError";
        reject(abortError);
        return;
      }

      // determine which http(s) client to use
      // const isSSL = request.protocol === "https:";
      // const agent = isSSL ? this.config.httpsAgent : this.config.httpAgent;

      // If the request is taking a long time, check socket usage and potentially warn.
      // This warning will be cancelled if the request resolves.
      // timeouts.push(
      //   timing.setTimeout(
      //     () => {
      //       this.socketWarningTimestamp = NodeHttpHandler.checkSocketUsage(
      //         agent,
      //         this.socketWarningTimestamp,
      //         this.config!.logger
      //       );
      //     },
      //     this.config.socketAcquisitionWarningTimeout ??
      //       (this.config.requestTimeout ?? 2000) + (this.config.connectionTimeout ?? 1000)
      //   )
      // );

      const queryString = buildQueryString(request.query || {});
      // let auth = undefined;
      let requestHeader = {
        ...request.headers
      }
      if (request.username != null || request.password != null) {
        const username = request.username ?? "";
        const password = request.password ?? "";
        // auth = `${username}:${password}`;
        const textEncoder = new util.TextEncoder();
        const credentials = textEncoder.encodeInto(`${username}:${password}`);
        const authHeader = 'Basic ' + new util.Base64Helper().encodeToStringSync(credentials);
        requestHeader['Authorization']=authHeader
      }
      let path = request.path;
      if (queryString) {
        path += `?${queryString}`;
      }
      if (request.fragment) {
        path += `#${request.fragment}`;
      }

      let hostname = request.hostname ?? "";
      if (hostname[0] === "[" && hostname.endsWith("]")) {
        hostname = request.hostname.slice(1, -1);
      } else {
        hostname = request.hostname;
      }
      const body = await this.getBufferedBody(request.body)
      const nodeHttpsOptions: http.HttpRequestOptions = {
        header: requestHeader,
        // host: hostname,
        method: request.method,
        // path,
        // port: request.port,
        // agent,
        // auth,
        connectTimeout: this.config.connectionTimeout,
        readTimeout: this.config.requestTimeout,
        extraData:body
      };
      let port=request.port?':'+request.port:''
      let url = `${request.protocol}//${hostname}${port}${path}`
      let httpRequest = http.createHttp();
      console.debug('httpHandler','url='+url)
      console.debug('httpHandler','options='+JSON.stringify(nodeHttpsOptions))
      httpRequest.request(url,nodeHttpsOptions,(err,res)=>{
        if (err) {
          console.error('httpHandler','err='+JSON.stringify(err))
          if (err.code ===2300028) {
            reject(Object.assign(err, { name: "TimeoutError" }));
          }else{
            reject(err)
          }
        }else{
          console.info('httpHandler','res='+JSON.stringify(res))
          const httpResponse = new HttpResponse({
            statusCode: res.responseCode,
            reason: "",
            headers: getTransformedHeaders(res.header),
            body: res.result,
          });
          console.error('httpHandler','return '+JSON.stringify(httpResponse))
          resolve({ response: httpResponse });
        }
      })
      // create the http request
      // const requestFunc = isSSL ? hsRequest : hRequest;
      //
      // const req = requestFunc(nodeHttpsOptions, (res) => {
      //   const httpResponse = new HttpResponse({
      //     statusCode: res.statusCode || -1,
      //     reason: res.statusMessage,
      //     headers: getTransformedHeaders(res.headers),
      //     body: res,
      //   });
      //   resolve({ response: httpResponse });
      // });

      // httpRequest.on("error", (err: Error) => {
      //   if (NODEJS_TIMEOUT_ERROR_CODES.includes((err as any).code)) {
      //     reject(Object.assign(err, { name: "TimeoutError" }));
      //   } else {
      //     reject(err);
      //   }
      // });

      // wire-up abort logic
      if (abortSignal) {
        const onAbort = () => {
          // ensure request is destroyed
          httpRequest.destroy();
          const abortError = new Error("Request aborted");
          abortError.name = "AbortError";
          reject(abortError);
        };
        // if (typeof (abortSignal as AbortSignal).addEventListener === "function") {
        //   // preferred.
        //   const signal = abortSignal as AbortSignal;
        //   signal.addEventListener("abort", onAbort, { once: true });
        //   req.once("close", () => signal.removeEventListener("abort", onAbort));
        // } else {
          // backwards compatibility
          abortSignal.onabort = onAbort;
        // }
      }

      // Defer registration of socket event listeners if the connection and request timeouts
      // are longer than a few seconds. This avoids slowing down faster operations.
      // timeouts.push(setConnectionTimeout(httpRequest, reject, this.config.connectionTimeout));
      // timeouts.push(setSocketTimeout(req, reject, this.config.requestTimeout));

      // Workaround for bug report in Node.js https://github.com/nodejs/node/issues/47137
      // const httpAgent = nodeHttpsOptions.agent;
      // if (typeof httpAgent === "object" && "keepAlive" in httpAgent) {
      //   timeouts.push(
      //     setSocketKeepAlive(req, {
      //       // @ts-expect-error keepAlive is not public on httpAgent.
      //       keepAlive: (httpAgent as hAgent).keepAlive,
      //       // @ts-expect-error keepAliveMsecs is not public on httpAgent.
      //       keepAliveMsecs: (httpAgent as hAgent).keepAliveMsecs,
      //     })
      //   );
      // }

      // writeRequestBodyPromise = writeRequestBody(httpRequest, request, this.config.requestTimeout).catch((e) => {
      //   timeouts.forEach(timing.clearTimeout);
      //   return _reject(e);
      // });
    });
  }

  updateHttpClientConfig(key: keyof NodeHttpHandlerOptions, value: NodeHttpHandlerOptions[typeof key]): void {
    this.config = undefined;
    this.configProvider = this.configProvider.then((config) => {
      return {
        ...config,
        [key]: value,
      };
    });
  }

  httpHandlerConfigs(): NodeHttpHandlerOptions {
    return this.config ?? {};
  }

  async getBufferedBody(body?: string | ArrayBuffer | ArrayBufferView | stream.Readable | Uint8Array): Promise<ArrayBuffer|string>{
    if (!body){
      return undefined
    }else if(typeof body==='string'){
      return body
    }else if (body instanceof stream.Readable) {
      const buffer =  await streamCollector(body)
      return buffer.buffer
    }else if (body instanceof ArrayBuffer){
      return body
    }else if (ArrayBuffer.isView(body)){
      return body.buffer
    }else{
      const uint8 = body as Uint8Array;
      if (
        typeof uint8 === "object" &&
        uint8.buffer &&
        typeof uint8.byteOffset === "number" &&
        typeof uint8.byteLength === "number"
      ) {
        return buffer.from(uint8.buffer, uint8.byteOffset, uint8.byteLength).buffer;
      }else{
        buffer.from(body as unknown as ArrayBuffer).buffer
      }
    }
  }
}
